Skip to content

Conversation

@andriyDev
Copy link
Contributor

Objective

Solution

  • Allow users to build SavedAsset instances by building up their labeled assets. This can either take an existing handle + asset ref, or will create a handle for your asset ref.
  • Provide a function to correctly call the AssetSaver, and write out the correct meta file.

Some things I am leaving to future PRs

  • Making it easier to access subassets in an AssetSaver.
  • Making a nicer API for asset saving (e.g., registering asset savers and looking up the appropriate saver for type ID + extension).

Weird implementation details

  • I needed to create my own Cow-like type (which I named Moo), since I needed to store just a ref or owned hashmap. I wrote a lengthy comment explaining why it's needed (TL;DR variance is complicated, Cow doesn't work). Note: it's possible we could just use CowArc instead, but we never need to clone the map so this seems like overkill. Also having a nice place to explain the variance problems is useful here.
  • For the builder, I needed to add an extra lifetime to add_labeled_asset_with_*<'b: 'a>, since otherwise, the CowArc gets coerced to 'static which means the lifetime of the subasset needs to be 'static, which practically makes this unusable. By adding a second lifetime dedicated to the CowArc,

Testing

  • Created a new example: this allows you to make a small "scene" of boxes, and save and load it to your heart's content!
  • Added a couple simple tests.

@andriyDev andriyDev added C-Feature A new feature, making something new possible A-Assets Load files from disk to use for things like images, models, and sounds D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 21, 2026
@andriyDev andriyDev added this to the 0.19 milestone Jan 21, 2026
@andriyDev andriyDev requested a review from kristoff3r January 21, 2026 10:10
@alice-i-cecile alice-i-cecile added the M-Release-Note Work that should be called out in the blog due to impact label Jan 21, 2026
@andriyDev andriyDev requested a review from greeble-dev January 22, 2026 16:54
let asset_server = app.world().resource::<AssetServer>().clone();
let mut saved_asset_builder =
SavedAssetBuilder::new(asset_server.clone(), "some/target/path.cool.ron".into());
saved_asset_builder.add_labeled_asset_with_existing_handle(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what happens if we're wrong about whether or not we already have a handle?

EG, say we don't realize this asset is already saved, and we save over it with a new labeled asset sans a handle.

What breaks, and does it break predictably?

Copy link
Contributor Author

@andriyDev andriyDev Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's clear what "existing" means is not quite clear. "Existing" in this context means that your root asset already contains a handle, whose asset you want to include in the saved asset as a subasset.

So if you save multiple times, you can keep calling add_labeled_asset_with_new_handle. It's just "more work" to do so with add_labeled_asset_with_new_handle since you need to store that handle in the root asset.

So like, the wrong thing to do would be to do something like:

  1. Clone an asset that is stored in Assets.
  2. For each of its subassets, you call add_labeled_asset_with_new_handle - and then do nothing with the handle.
  3. Call save.

This is wrong because now the asset that you cloned out of Assets will contain handles that don't have a corresponding subasset in the SavedAsset - the handles won't match. So from the perspective of the AssetSaver it will see handles that don't match what they get when they call get_handle(subasset_label). In a future PR, we should also have a get_by_handle - which will just return you None or something. The correct thing to do in this example is in step 2 to either use add_labeled_asset_with_existing_handle, or you could call add_labeled_asset_with_new_handle and store that handle in the root asset.

Btw, a missing handle is not necessarily a mistake - for example, if you have a StandardMaterial, the handles it stores are most likely just references to other files. In other words, if you accidentally use add_labeled_asset_with_new_handle and then don't store that handle, the AssetSaver may interpret that handle as a nested-loaded asset, and just serialize its path.

Edit: Added more documentation to explain when to use either function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually even that is a little wrong, since it's possible for an Asset to not hold handles to its subassets - the AssetSaver can still save these by iterating through the labeled assets in the SavedAsset.

But in the common case, the root asset stores handles, so we should optimize for that.

Copy link
Contributor

@IronGremlin IronGremlin Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's part of my concern, and this seems like a good way to ensure that data passed through the save operation to a given subpath is the data that later gets referenced during load for a given subpath.

I am also concerned with data and handles that might have existed BEFORE that save operation though.

Consider the following:

  1. Asset exists with subassets in their respective asset stores. Someone calls asset_server.load("a_path.asset#subassetpath"). They store that handle somewhere, on an entity.
  2. Someone later saves an asset and associated subassets using add_labeled_asset_with_new_handle.
  3. Also, someone now, a second time, after the asset has been saved, calls asset_server.load("a_path.asset#subassetpath")

This presents two questions:

  1. As of step two, does the reference pointed to by the handle in step 1 now contain new data?
  2. Are the handles returned in step 1 and step 3 clones of the same handle?

I -think- that the answer to these questions is "yes," but we should assert this behavior under test

Copy link
Contributor

@greeble-dev greeble-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm clicking approve as the PR does what it says and is backed up by tests and an example. I left a few minor suggestions.

I do have some concerns with the overall direction:

  1. The code is significantly complicated by having to support handles and avoid copies. I'm not sure this complexity is worth it right now.
  2. We're not dogfooding since the engine doesn't have a complicated saver built-in - the example code is fairly contrived. This makes it hard to predict if the API will work well.
  3. My guess is that the subtleties around sub-assets and when to use add_labeled_asset_with_new_handle versus add_labeled_asset_with_existing_handle will be challenging for users.

I don't think these concerns are blocking, but maybe they deserve more discussion. And as ever, there's the possibility that assets-as-entities ends up remaking the whole thing.

CowArc<'static, str>: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
pub fn get_labeled<B: Asset>(&self, label: &str) -> Option<SavedAsset<'a, '_, B>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change means get_labeled can no longer be called directly with a CowArc<str>. Only a minor regression, but is it necessary? I guess the lifetimes might be tricky.

@@ -0,0 +1,372 @@
//! This example demonstrates how to save assets.
Copy link
Contributor

@greeble-dev greeble-dev Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth calling the example asset_saving_subassets.rs? Or subasset_saving.rs? It's a fairly complicated example, so this would leave space for a simpler example that doesn't involve sub-assets.


## 1. Building the `SavedAsset`

To build the `SavedAsset`, either use `SavedAsset::from_asset`, or `SavedAssetBuilder`. For example:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could do with a brief example of SavedAsset::from_asset that's separate from the SavedAssetBuilder example? Would make clear that SavedAssetBuilder is only needed for more complicated cases.

Comment on lines +522 to +525
// NOTE: We can't handle embedded dependencies in any way, since we need to write to
// another file to do so.
embedded_dependencies: vec![],
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// NOTE: We can't handle embedded dependencies in any way, since we need to write to
// another file to do so.
embedded_dependencies: vec![],
};
embedded_dependencies: vec![],
};
// NOTE: We can't handle embedded dependencies in any way, since we need to write to
// another file to do so.
assert!(asset.embedded_dependencies.is_empty());

Assert seems safer if this is only used in tests.

.web-asset-cache
examples/large_scenes/bistro/assets/*
examples/large_scenes/caldera_hotel/assets/*
examples/asset/saved_assets
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this is for? The example asset+meta is already tracked by git, and the example doesn't add new ones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Assets Load files from disk to use for things like images, models, and sounds C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes M-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants